using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace NakedSanta
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        BasicEffect basicEffect;

        RasterizerState WIREFRAME_RASTERIZER_STATE;

        List<GameObject> gameObjects;

        FrameRateCounter fpsCounter;

        Copter copter;

        Matrix projection;
        Matrix view;

        //free first person camera 
        Vector3 cameraPosition = new Vector3(20.0f, 0.0f, 0.0f);
        float leftrightRot = MathHelper.PiOver2;
        float updownRot = -MathHelper.Pi / 10.0f;
        const float rotationSpeed = 0.3f;
        const float moveSpeed = 30.0f;
        MouseState originalMouseState;

        //third person camera
        float cameraDelaySpeed = 0.03f;
        Vector3 thirdPersonReference = new Vector3(1.8f, 0.5f, 0f);
        Quaternion cameraRotation;

        bool noclip = false;
        bool keyPressed = false;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferMultiSampling = true;
            //Disable fps limit
            graphics.SynchronizeWithVerticalRetrace = true;
            IsFixedTimeStep = true;

            Content.RootDirectory = "Content";

            fpsCounter = new FrameRateCounter(this);
            Components.Add(fpsCounter);
            fpsCounter.ShowFPS = true;

        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            WIREFRAME_RASTERIZER_STATE = new RasterizerState() { CullMode = CullMode.CullCounterClockwiseFace, FillMode = FillMode.Solid };

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            gameObjects = new List<GameObject>();

            copter = new Copter(graphics.GraphicsDevice);
            gameObjects.Add(copter);

            cameraRotation = copter.rotation;

            CreateWorld();

            foreach (GameObject gameObject in gameObjects)
                gameObject.LoadContent();

            UpdateViewMatrix();

            InitializeEffect();

            Mouse.SetPosition(graphics.GraphicsDevice.Viewport.Width / 2, graphics.GraphicsDevice.Viewport.Height / 2);
            originalMouseState = Mouse.GetState();

        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>

        protected override void Update(GameTime gameTime)
        {
            float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
            ProcessInput(elapsedTime);
            // TODO: Add your update logic here
            UpdateViewMatrix();

            foreach (GameObject go in gameObjects)
                go.Update(gameTime);

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Set states ready for 3D  
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            basicEffect.View = view;
            basicEffect.World = Matrix.Identity;
            basicEffect.CurrentTechnique.Passes[0].Apply();
            basicEffect.EnableDefaultLighting();

            foreach (GameObject gameObject in gameObjects)
                gameObject.Draw(basicEffect, Matrix.Identity);

            base.Draw(gameTime);
        }

        private void CreateWorld()
        {
            CreateGround();
            CreateFloor();
            CreateBuildings();
        }

        private void CreateGround()
        {
            Tile ground = new Tile(GraphicsDevice, Color.Green, new Vector3(0, -4.1f, 0));
            ground.scale = new Vector3(2000, 1, 2000);
            ground.position = new Vector3(-1000, -4.1f, 1000);
            gameObjects.Add(ground);
        }

        private void CreateBuildings()
        {
            Random rand = new Random();

            for (int x = -40; x < 40; x++)
            {
                for (int z = -40; z < 40; z++)
                {
                    //Randomize buildings.
                    if (rand.Next(100) < 10)
                        gameObjects.Add(new Building(GraphicsDevice, Color.Gray, rand.Next(6) + 2, new Vector3(x, -4, z)));
                }
            }
        }

        private void CreateFloor()
        {
            gameObjects.Add(new Ground(graphics.GraphicsDevice, new Vector3(-45, -4.0f, -45)));
        }

        private void InitializeEffect()
        {
            GraphicsDevice.RasterizerState = WIREFRAME_RASTERIZER_STATE;

            basicEffect = new BasicEffect(graphics.GraphicsDevice);
            basicEffect.VertexColorEnabled = true;
            basicEffect.Projection = projection;
            basicEffect.World = Matrix.Identity;
        }

        private void UpdateViewMatrix()
        {
            if (noclip)
                UpdateViewMatrixNoclip();
            else
                UpdateViewMatrixThirdPerson();
        }

        private void UpdateViewMatrixNoclip()
        {
            Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);

            Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
            Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
            Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget;

            Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);
            Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);

            float aspectRatio = GraphicsDevice.DisplayMode.AspectRatio;

            Matrix.CreatePerspectiveFieldOfView(
                MathHelper.PiOver4,
                aspectRatio,
                0.1f,
                1000.0f,
                out projection);

            view = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);
        }

        private void UpdateViewMatrixThirdPerson()
        {
            cameraRotation = Quaternion.Lerp(cameraRotation, copter.rotation, cameraDelaySpeed);
            //make the camera not follow the helicopters pitch
            Matrix rot = Matrix.CreateFromQuaternion(cameraRotation);
            rot.Up = Vector3.Up;
            cameraRotation = Quaternion.CreateFromRotationMatrix(rot);
            // Create a vector pointing the direction the camera is facing.
            Vector3 transformedReference = Vector3.Transform(thirdPersonReference, cameraRotation);
            // Calculate the position the camera is looking from.
            Vector3 cameraPosition = transformedReference + copter.position;
            if (cameraPosition.Y < -3.5f)
                cameraPosition.Y = -3.5f;

            Viewport viewport = graphics.GraphicsDevice.Viewport;
            float aspectRatio = (float)viewport.Width / (float)viewport.Height;

            Matrix.CreatePerspectiveFieldOfView(
                MathHelper.PiOver4,
                aspectRatio,
                0.1f,
                1000.0f,
                out projection);

            view = Matrix.CreateLookAt(cameraPosition, copter.position,
                new Vector3(0.0f, 1.0f, 0.0f));
        }

        private void AddToCameraPosition(Vector3 vectorToAdd)
        {
            Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
            Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation);
            cameraPosition += moveSpeed * rotatedVector;

            UpdateViewMatrix();
        }

        private void ProcessNoclipInput(float amount, KeyboardState keyState)
        {
            MouseState currentMouseState = Mouse.GetState();
            if (currentMouseState != originalMouseState)
            {
                float xDifference = currentMouseState.X - originalMouseState.X;
                float yDifference = currentMouseState.Y - originalMouseState.Y;
                leftrightRot -= rotationSpeed * xDifference * amount;
                updownRot -= rotationSpeed * yDifference * amount;
                Mouse.SetPosition(graphics.GraphicsDevice.Viewport.Width / 2, graphics.GraphicsDevice.Viewport.Height / 2);
            }

            Vector3 moveVector = new Vector3(0, 0, 0);
            if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W))
                moveVector += new Vector3(0, 0, -1);
            if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S))
                moveVector += new Vector3(0, 0, 1);
            if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D))
                moveVector += new Vector3(1, 0, 0);
            if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A))
                moveVector += new Vector3(-1, 0, 0);
            if (keyState.IsKeyDown(Keys.Space))
                moveVector += new Vector3(0, 1, 0);
            if (keyState.IsKeyDown(Keys.LeftShift))
                moveVector += new Vector3(0, -1, 0);

            AddToCameraPosition(moveVector * amount*.2f);
        }

        private void ProcessInput(float amount)
        {
            KeyboardState keyState = Keyboard.GetState();

            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            else if (keyState.IsKeyDown(Keys.Escape))
                this.Exit();

            if (noclip)
            {
                ProcessNoclipInput(amount, keyState);
            }
            else
            {
                copter.ProcessInput(amount);
            }

            //toggle noclip or fps
            if (!keyPressed && keyState.IsKeyDown(Keys.N))
            {
                noclip = !noclip;
                keyPressed = true;
                if (!noclip)
                    cameraRotation = copter.rotation;
                else
                    cameraPosition = copter.position + thirdPersonReference;
            }
            else if (!keyPressed && keyState.IsKeyDown(Keys.M))
            {
                fpsCounter.ShowFPS = !fpsCounter.ShowFPS;
                keyPressed = true;
            }

            UpdateViewMatrix();

            if (keyPressed)
            {
                if (keyState.IsKeyUp(Keys.N) && keyState.IsKeyUp(Keys.M))
                    keyPressed = false;
            }
        }
    }
}
